#include "ccms/nr/process.h"
#include "arlib/raii/file_handle.h"
#include "arlib/exception/invalid_argument_exception.h"
#include "arlib/exception/runtime_exception.h"
#include "arlib/exception/errno_exception.h"
#include <cerrno>
#include <cstring> // memset
#include <cstdlib>
#include <linux/version.h>

#define THROW_INVALIDARGUMENTEXCEPTION(reason) throw arlib::exception::InvalidArgumentException(__FILE__, __LINE__, reason)
#define THROW_RUNTIMEEXCEPTION(reason) throw arlib::exception::RuntimeException(__FILE__, __LINE__, reason)
#define THROW_ERRNOEXCEPTION(reason, errnum) throw arlib::exception::ErrnoException(__FILE__, __LINE__, reason, errnum)

using namespace ccms::nr;

#define FILENAME_MAX_LEN             128
#define LINE_BUF_LEN                 1024
#define COMMAND_NAME_MAX_LEN         16    // based on TASK_COMM_LEN from <linux/sched.h>
#define COMMAND_NAME_MAX_LEN_SSCANF  "15"

namespace ccms {
namespace nr {

Process::Process(void) : pid(0), userTimeRec(0), userTimePrev(0),
    systemTimeRec(0), systemTimePrev(0), rss(0), pss(0), exists(false)
{
}

Process::Process(const pid_t pid) : pid(0), userTimeRec(0), userTimePrev(0),
    systemTimeRec(0), systemTimePrev(0), rss(0), pss(0), exists(false)
{
    if(pid != 0) {
        this->pid = pid;
    } else {
        THROW_INVALIDARGUMENTEXCEPTION("Invalid pid");
    }
}

void Process::updateFromStat()
{
    bool  isFullyInitialized = !(this->commandName.empty());
    char  filename[FILENAME_MAX_LEN];
    char  lineBuf[LINE_BUF_LEN];
    char* lineBufCont = NULL;
    char  commandNameBuf[COMMAND_NAME_MAX_LEN];
    arlib::raii::FileHandle fh;

    memset(filename, '\0', sizeof(filename));
    snprintf(filename, sizeof(filename) / sizeof(char), "/proc/%ld/stat", (long) pid);

    fh.reset(fopen(filename, "rb"));
    if(!fh.get()) {
        THROW_ERRNOEXCEPTION("Could not open /proc/<PID>/stat", errno);
    }

    memset(lineBuf, '\0', sizeof(lineBuf));
    if(!fgets(lineBuf, sizeof(lineBuf) / sizeof(char), fh.get())) {
        THROW_ERRNOEXCEPTION("Could not read from /proc/<PID>/stat", errno);
    }

    userTimePrev = userTimeRec;
    systemTimePrev = systemTimeRec;

    lineBufCont = strchr(lineBuf, ')');
    if(lineBufCont != NULL) {
        *lineBufCont = '\0';
        lineBufCont += 2; // ommit the ') ' sequence of characters
    } else {
        THROW_RUNTIMEEXCEPTION("Could not match the ')' character - invalid format of /proc/<PID>/stat file");
    }

    if(isFullyInitialized == true) {
        if(sscanf(lineBufCont, "%*c %*d %*d %*d %*d %*d %*u %*u "
                "%*u %*u %*u %lu %lu %*d %*d %*d %*d %*d %*d %*u %*u "
                "%ld", &userTimeRec, &systemTimeRec, &rss) != 3) {
            THROW_RUNTIMEEXCEPTION("Could not update data from /proc/<PID>/stat");
        }
    } else {
        memset(commandNameBuf, '\0', sizeof(commandNameBuf));
        if(sscanf(lineBuf, "%*d (%" COMMAND_NAME_MAX_LEN_SSCANF "s ",
                commandNameBuf) != 1) {
            THROW_RUNTIMEEXCEPTION("Could not retrieve command name");
        }

        this->commandName = commandNameBuf;

        if(sscanf(lineBufCont, "%*c %*d %*d %*d %*d %*d %*u %*u %*u "
                "%*u %*u %lu %lu %*d %*d %*d %*d %*d %*d %*u %*u %ld",
                &userTimeRec, &systemTimeRec, &rss) != 3) {
            THROW_RUNTIMEEXCEPTION("Could not update data from /proc/<PID>/stat");
        }
    }
}

void Process::updateFromSmaps(void) {
    arlib::raii::FileHandle fh;
    char filename[FILENAME_MAX_LEN];
    char lineBuf[LINE_BUF_LEN];
    char* lineBufCont = NULL;
    unsigned long buffer = 0;

    pss = 0;

    memset(filename, '\0', sizeof(filename));
    snprintf(filename, sizeof(filename) / sizeof(char), "/proc/%ld/smaps", (long) pid);

    fh.reset(fopen(filename, "rb"));
    if(!fh.get()) {
        THROW_ERRNOEXCEPTION("Could not open /proc/<PID>/smaps", errno);
    }

    do {
        memset(lineBuf, '\0', sizeof(lineBuf));
        if(!fgets(lineBuf, sizeof(lineBuf) / sizeof(char), fh.get())) {
            if(feof(fh.get())) {
                break;
            } else {
                THROW_ERRNOEXCEPTION("Could not read from /proc/<PID>/stat", ferror(fh.get()));
            }
        }

        if(sscanf(lineBuf, "Pss: %lu", &buffer) == 1) {
            pss += (long) buffer;
        }
    } while(!feof(fh.get()));
}

void Process::update(void) {
    this->updateFromStat();
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,25))
    this->updateFromSmaps();
#endif
}

} // namespace nr
} // namespace ccms
